
package com.cluedo;

import java.util.Iterator;
import java.util.List;

public class DataGrid {
    public static DataGrid newInstance(int playerRows, int gameTokenCols,
            String[] gameTokens, CluedoDataPad main, Player player) {
        float[][] grid = new float[playerRows][gameTokenCols];

        for (String token : player.getTokens()) {
            if (CluedoDataPad.weapons.contains(token)) {
                player.weaponCategoryCnt++;
            } else if (CluedoDataPad.rooms.contains(token)) {
                player.roomCategoryCnt++;
            } else if (CluedoDataPad.characters.contains(token)) {
                player.charCategoryCnt++;
            }
        }

        // Known Tokens/Cards that Player have
        for (String token : player.getTokens()) {
            int tokenCol = CluedoDataPad.tokens.get(token);
            grid[player.id][tokenCol] = 1;
            for (int row = 0; row < grid.length; row++) {
                if (row != player.id) {
                    grid[row][tokenCol] = 0;
                }
            }
        }

        // Known Tokens/Cards that Player doesn't have part 1.2
        setInitialConditionalProb(grid, player, player.weaponCategoryCnt,
                CluedoDataPad.weapons);
        setInitialConditionalProb(grid, player, player.roomCategoryCnt,
                CluedoDataPad.rooms);
        setInitialConditionalProb(grid, player, player.charCategoryCnt,
                CluedoDataPad.characters);

        return new DataGrid(grid, main, player);
    }

    private static void setInitialConditionalProb(float[][] grid,
            Player player, int playerCategoryCnt, List<String> category) {
        int cardCnt = player.cards.size();
        float distriPlayers = (float) (category.size() - playerCategoryCnt - 1)
                / (category.size() - playerCategoryCnt);
        float cardCondi = 1f / (CluedoDataPad.totalTokens - 3 - cardCnt);
        float prob = cardCnt * (distriPlayers * cardCondi);
        for (String token : category) {
            if (!player.cards.contains(token)) {
                int tokenCol = CluedoDataPad.tokens.get(token);
                for (int row = 0; row < grid.length; row++) {
                    if (row == grid.length - 1) {
                        grid[row][tokenCol] = 1f / (category.size() - playerCategoryCnt);
                    } else if (row != player.id) {
                        grid[row][tokenCol] = prob;
                    }
                }
            }
        }
    }

    private final float[][] grid;
    private final Player player;

    private DataGrid(float[][] grid, CluedoDataPad main, Player player) {
        this.grid = grid;
        this.player = player;
    }

    public String getSuggestion() {
        String suggestion = String.format("%s,%s,%s",
                getCategorySuggestion(CluedoDataPad.weapons),
                getCategorySuggestion(CluedoDataPad.rooms),
                getCategorySuggestion(CluedoDataPad.characters));
        System.out.println("Plyr" + player.id + " Suggests:\t" + suggestion + "\n");
        return suggestion;
    }

    public void updateToken(String cardName, int playerRequest, int playerResponse) {
        List<String> category = CluedoDataPad.determineTokenFrom(cardName);
        Integer tokenCol = CluedoDataPad.tokens.get(cardName);
        if (tokenCol != null) {
            confirmTokenInPlayer(tokenCol, playerRequest, playerResponse, category);
        }
    }

    private String getCategorySuggestion(List<String> category) {
        String cardSuggested = "";
        float likelyProb = 0f;
        Iterator<String> it = category.iterator();
        while (it.hasNext()) {
            String token = it.next();
            int tokCol = CluedoDataPad.tokens.get(token);
            for (int plyr = 0; plyr < grid.length; plyr++) {
                if (plyr != player.id && grid[plyr][tokCol] > likelyProb &&
                        grid[plyr][tokCol] != 1f) {
                    cardSuggested = token;
                    likelyProb = grid[plyr][tokCol];
                }
            }
        }
        return cardSuggested;
    }

    public void updateStatisticsGivenResponse(int playerReq, int playerResp,

            String[] request, String tokenResponse) {

        System.err.println(String.format("Ply:%d\tPlyReq:%d\tPlyRes:%d\tTok:%s,%s,%s\t%s",
                player.id, playerReq, playerResp,
                request[0], request[1], request[2], tokenResponse));
        if (playerReq == player.id) {
            List<String> category = CluedoDataPad.determineTokenFrom(tokenResponse);

            int tokenCol = CluedoDataPad.tokens.get(tokenResponse);

            boolean fromNextPlayer = false;
            int playerReqNextPlayer = playerReq + 1;
            if (playerReqNextPlayer >= grid.length - 1) {
                playerReqNextPlayer = 0;

            }
            if (playerReqNextPlayer == playerResp) {
                fromNextPlayer = true;
            }
            if (fromNextPlayer) {
                // zero out neighbor response
                System.err.println(String.format("From Plyr %s To Plyr %d",
                        playerReq, playerResp));
                confirmTokenInPlayer(tokenCol, playerReq, playerResp, category);
            } else {
                System.err.println(String.format("From Plyr %s Not Plyr %d",
                        playerReq, playerResp));
                // zero out player responses and distribute known player info
                confirmTokenNotInPlayers(playerReq, playerResp, request);
                confirmTokenInPlayer(tokenCol, playerReq, playerResp, category);
            }
        } else {
            // Another Player's Request - Soft Requests
            System.err.println(String.format("From Plyr %s to Plyr %d Not me %d",
                    playerReq, playerResp, player.id));
            if (playerResp != player.id) {
                if (playerResp != playerReq + 1) {
                    // Players till playerResponse don't have these card
                    confirmTokenNotInPlayers(playerReq, playerResp, request);
                } else {
                    boolean considerSoftProb = true;
                    int nonZeroCards = 0;
                    for (String tokenReq : request) {
                        int tokCol = CluedoDataPad.tokens.get(tokenReq);
                        if (grid[playerResp][tokCol] == 1f) {
                            considerSoftProb = false;
                            break;
                        }
                        if (grid[playerResp][tokCol] != 0f &&
                                grid[playerResp][tokCol] != 1f) {
                            nonZeroCards++;
                        }
                    }
                    if (considerSoftProb) {
                        for (String tokenReq : request) {
                            int tokenCol = CluedoDataPad.tokens.get(tokenReq);
                            boolean ignoreToken = false;
                            float[] prevCols = new float[grid.length];
                            for (int row = 0; row < grid.length; row++) {
                                prevCols[row] = grid[row][tokenCol];
                                if (grid[row][tokenCol] == 1f) {
                                    ignoreToken = true;
                                    break;
                                }
                            }

                            if (!ignoreToken) {
                                float newProb = (1f - prevCols[playerResp]) / nonZeroCards;
                                float diffValue = prevCols[playerResp] - newProb;

                                int distrCol = countGTZerosCol(grid, playerResp, tokenCol);
                                float distrProbCol = (float) diffValue / distrCol;

                                grid[playerResp][tokenCol] = newProb;
                                distriProbCol(playerResp, tokenCol, distrProbCol);
                                for (int row = 0; row < grid.length; row++) {
                                    int distrRow = countGTZerosRow(grid, row, tokenCol);
                                    float diffValue1 = prevCols[row] - grid[row][tokenCol];
                                    float distrProb = diffValue1 / distrRow;

                                    if (diffValue1 != 0f && distrRow != 0f) {
                                        for (int tokCol = 0; tokCol < grid[0].length; tokCol++) {
                                            if (tokCol != tokenCol && grid[row][tokCol] != 0f
                                                    && grid[row][tokCol] != 1f)
                                                grid[row][tokCol] += distrProb;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        System.err.println("Updated Statistics Complete");
    }

    public void narrowProbSection(List<String> category) {

        int tokenCountOneZero = 0;
        int categoryCount = 0;
        int tokenColWithProbRemaining = 0;
        Iterator<String> it = category.iterator();
        while (it.hasNext()) {
            String tok = it.next();
            int tokCol = CluedoDataPad.tokens.get(tok);
            for (int row = 0; row < grid.length; row++) {
                if (grid[row][tokCol] == 0f ||
                        grid[row][tokCol] == 1f) {
                    tokenCountOneZero++;
                } else {
                    tokenColWithProbRemaining = tokCol;
                }
                if (grid[row][tokCol] < 0f) {
                    // grid[row][tokCol] = 0;
                    row = 0; // Reset iteration
                    tokenCountOneZero = 0;
                }
                if (tokenCountOneZero == grid.length - 1) {
                    categoryCount++;
                }
            }
            tokenCountOneZero = 0;
        }
        // Narrow this section since other categories are known
        if (categoryCount == category.size() - 1) {
            float[] prevCols = new float[grid.length];
            for (int row = 0; row < grid.length; row++) {
                prevCols[row] = grid[row][tokenColWithProbRemaining];
                if (row == grid.length - 1) {
                    grid[row][tokenColWithProbRemaining] = 1f;
                } else {
                    grid[row][tokenColWithProbRemaining] = 0f;
                }
            }

            for (int row = 0; row < grid.length; row++) {
                int distrRow = countGTZerosRow(grid, row, tokenColWithProbRemaining);
                float diffValue = prevCols[row] - grid[row][tokenColWithProbRemaining];
                float distrProb = diffValue / distrRow;

                if (diffValue != 0f && distrRow != 0f) {
                    for (int tokCol = 0; tokCol < grid[0].length; tokCol++) {
                        if (tokCol != tokenColWithProbRemaining && grid[row][tokCol] != 0f
                                && grid[row][tokCol] != 1f)
                            grid[row][tokCol] += distrProb;
                    }
                }
            }
        }
    }

    /**
     * Normalizes the affected player's columns and performals normalization on affected rows. This
     * function iterates on each player until it reaches the player response
     * 
     * @param playerRequest - requester
     * @param playerResponse - responder
     * @param requests - Cluedo player requests
     */
    private void confirmTokenNotInPlayers(int playerRequest,
            int playerResponse, String[] requests) {

        for (int tok = 0; tok < requests.length; tok++) {
            int tokenCol = CluedoDataPad.tokens.get(requests[tok]);

            for (int plyr = playerRequest + 1; plyr != playerResponse; plyr++) {

                if (plyr >= grid.length - 1) {
                    plyr = 0;
                }

                if (plyr == playerResponse) {
                    break;
                }

                float prevValue = grid[plyr][tokenCol];
                if (prevValue != 0f) {
                    if (grid[plyr][tokenCol] != 1f) {
                        grid[plyr][tokenCol] = 0;
                    }

                    int distrCol = countGTZerosCol(grid, plyr, tokenCol);
                    int distrRow = countGTZerosRow(grid, plyr, tokenCol);

                    float distrProbCol = (float) prevValue / distrCol;
                    float distrProbRow = (float) prevValue / distrRow;

                    distriProbCol(plyr, tokenCol, distrProbCol);
                    distriProbRowNormalized(plyr, tokenCol, distrProbRow);
                }
            }
        }
    }

    /**
     * Sets that Card to the player and zeros out the other players Performs normalization for the
     * affect cards (columns) based on the diff player's Response card's previous probability
     * 
     * @param tokenCol
     * @param playerRequest
     * @param playerResponse
     * @param category
     */
    private void confirmTokenInPlayer(int tokenCol, int playerRequest,
            int playerResponse, List<String> category) {
        float[] prevCols = new float[grid.length];
        for (int row = 0; row < grid.length; row++) {
            prevCols[row] = grid[row][tokenCol];
            if (row == playerResponse) {
                grid[row][tokenCol] = 1f;
            } else {
                grid[row][tokenCol] = 0f;
            }
        }

        for (int row = 0; row < grid.length; row++) {
            int distrRow = countGTZerosRow(grid, row, tokenCol);
            float diffValue = prevCols[row] - grid[row][tokenCol];
            float distrProb = diffValue / distrRow;

            if (diffValue != 0f && distrRow != 0f) {
                for (int tokCol = 0; tokCol < grid[0].length; tokCol++) {
                    if (tokCol != tokenCol && grid[row][tokCol] != 0f && grid[row][tokCol] != 1f)
                        grid[row][tokCol] += distrProb;
                }
            }
        }
    }

    /**
     * Normalized probability when distributing across rows. This will normalize the affected column
     * once the affected [row][column] is done
     * 
     * @param playerRow - player to ignore - normalizing from
     * @param tokenCol - column to ignore - normalizing from
     * @param distrProb - distributed Prob value
     * @param category - categories affected
     */
    private void distriProbRowNormalized(int playerRow, int tokenCol,
            float distrProb) {
        for (int col = 0; col < grid[0].length; col++) {
            if (tokenCol != col && grid[playerRow][col] != 0f
                    && grid[playerRow][col] != 1f) {
                grid[playerRow][col] += distrProb;
                int distrCol = countGTZerosCol(grid, playerRow, col);
                float distrValue = distrProb / distrCol;
                for (int row = 0; row < grid.length; row++) {
                    if (row != playerRow && grid[row][col] != 0f
                            && grid[row][col] != 1f) {
                        grid[row][col] -= distrValue;
                    }
                }
            }
        }
    }

    /**
     * Distributes Across Columns - ignores 1/0
     * 
     * @param playerRow - player to ignore - adding from
     * @param tokCol - column adding to
     * @param distrProb - value adding to column
     */
    private void distriProbCol(int playerRow, int tokCol, float distrProb) {
        for (int row = 0; row < grid.length; row++) {
            if (row != playerRow && grid[row][tokCol] != 0f
                    && grid[row][tokCol] != 1f) {
                grid[row][tokCol] += distrProb;
            }
        }
    }

    /**
     * Counts the given array for non 0/1 values for the given row
     * 
     * @param grid
     * @param row - reading this row
     * @param col - ignore this column
     * @param category
     * @return
     */
    private static int countGTZerosRow(float[][] grid, int row, int col) {
        int count = 0;
        for (int tokCol = 0; tokCol < grid[0].length; tokCol++) {
            if (tokCol != col && grid[row][tokCol] != 0f
                    && grid[row][tokCol] != 1f) {
                count++;
            }
        }
        return count;
    }

    /**
     * Counts the given array for non 0/1 values
     * 
     * @param grid
     * @param targetRow - ignores this row
     * @param col - counting on this column
     * @return
     */
    private static int countGTZerosCol(float[][] grid, int targetRow, int col) {
        int count = 0;
        for (int row = 0; row < grid.length; row++) {
            if (targetRow != row && grid[row][col] != 0f
                    && grid[row][col] != 1f) {
                count++;
            }
        }
        return count;
    }

    public float[][] getGrid() {
        return grid;
    }

    public String printGrid() {
        return printGrid(grid, player.id);
    }

    public static String printGrid(float[][] grid, int plyrId) {
        StringBuilder sb = new StringBuilder();
        sb.append("Id : " + plyrId + "\n");
        sb.append("\t");
        for (String token : CluedoDataPad.entryKeys) {
            sb.append(String.format("%.4s\t", token));
        }
        sb.append("\n");
        for (int row = 0; row < grid.length; row++) {
            if (row != grid.length - 1) {
                sb.append(String.format("Ply %d\t", row));
            } else {
                sb.append(String.format("Solut\t"));
            }
            for (int col = 0; col < grid[0].length; col++) {
                sb.append(String.format("%.4f\t", grid[row][col]));
            }
            sb.append("\n");
        }
        // System.out.println(sb.toString());
        return sb.toString();
    }

}
